module AdjustedCyclomaticComplexity

/*
	Author         	Arie van der Veek
 	Date           	December 11th 2011
	Description    	Calculates the adjusted cyclometric complexity of class
	               	written in Java. It calculated the cyclomatic complexity in the same way
					as in the module CyclomaticComplexity.rsc, but applies the rules
					described in the paper "Getting the Numbers Right" written by
					Arie van der Veek for Software Evolution @ UvA.
					
					In short it applies rules to an if statement to exclude mapping code
					for optional field which have been programmed with a certain nano pattern  				
 */

import lang::java::jdt::Java;
import lang::java::jdt::JDT;
import lang::java::jdt::JavaADT;

import Graph;
import Set;
import List;
import IO;
import String;

// Get the cyclometric complexity of the function given in the AstNode tree
public int calculateAdjustedCC(AstNode tree)
{
	int cc = 1;// basecount of CC is always one for one default control flow.
	// all cases that have to be taken into account for the cyclometric complexity
	visit(tree) {
		// Conditionals
		case ifStatement(AstNode booleanExpression, AstNode thenStatement, Option[AstNode] elseStatement):{
			if (!isOptionalMappingPattern( booleanExpression, thenStatement, elseStatement)) cc += 1;
		}
		case switchCase(bool isDefault, _): cc += 1;
		case conditionalExpression(_, _, _): cc += 1;
		case infixExpression(str operator, _, _, _):  {if ((operator == "||") || (operator == "&&")) cc += 1;}
		// Branching statements
		case breakStatement(_) :cc += 1;
	    case continueStatement(_) :cc += 1;
		// Iterative loops
		case whileStatement(_, _): cc += 1;
		case forStatement(_, _, _, _):cc += 1;
		case enhancedForStatement(_, _, _):cc += 1;
		case doStatement(_, _): cc += 1;
		// Exception handling statements, these also cause extra execution paths. Except the finally
		case throwStatement(_): cc += 1;
		case catchClause(_, _): cc += 1;
	};
	return cc;
}

// Checks the rules apply for the pattern 
private bool isOptionalMappingPattern(AstNode booleanExpression, AstNode thenStatement, Option[AstNode] elseStatement){
// Rule: There may not be an else part
	switch(elseStatement){
		case none(): ;// do nothing, it met the rule
		default : {	return false;} // there is an else part, so rule does not apply
	}

	str boolExprFinalInvokedMethod = "";
	list[str] boolExprcallChain = [];
	switch(booleanExpression){
		//Rule: There may only be one statement in the booleanExpression
		case infixExpression(_, _, _, _): return false; // found infix, thus indicating more then one expression in booleanExpression, so rule does not apply
		// Rule: The booleanExpression contains a method call and the method calls starts with has.
		case methodInvocation(Option[AstNode] optionalExpression, list[AstNode] genericTypes, str name, list[AstNode] typedArguments): {
			boolExprFinalInvokedMethod = name;
			boolExprcallChain = buildMethodCallList(optionalExpression);
			if (!startsWith(boolExprFinalInvokedMethod,"has")) return false;
		}
		default: return false; // it is not a method invocation, so return false.
	}
	//println("Boolean Expr Final Method: <boolExprFinalInvokedMethod> CallChain: <boolExprcallChain>");
	switch(thenStatement){
		// Rule: the thenStatement block may only contain ONE statement, if so apply the subrules to this statement
		case block(list[AstNode] statements): {
			//println("applyRule: <applyRulesToThenStatement(statements, boolExprFinalInvokedMethod, boolExprcallChain)>");
			bool thenStatementRulesApply = applyRulesToThenStatement(statements, boolExprFinalInvokedMethod, boolExprcallChain);  
			//println("thenStatementRulesApply: <thenStatementRulesApply>");
			if (!thenStatementRulesApply) return false;
		}
	}
	//println("then statement: <thenStatement>");
	//println("The statements match the rules, so return true.");
	return true;
}

/* Checks if the then statement matches with the following rules:
	1) The one statement in the block is an expression statement and
	2) It is a method invocation and
	3) there is only one typed argument given to the method invocation and
	4) the parameter is the get variant on the has method invocation 
	   of the previous boolean expression
	   
	   TODO: Clean up method, to many things going on in this method and it is WAY to complex. It works for now though......
*/  
private bool applyRulesToThenStatement(list[AstNode] statements, str boolExprFinalInvokedMethod, list[str] boolExprcallChain){
	if (size(statements) == 1) {
		switch(statements[0]){ // check if there is an expression statement in the first statement. Already
			case expressionStatement(AstNode expression): {
				switch (expression){// check if the expression is a method invocation
					case methodInvocation(Option[AstNode] optionalExpression, list[AstNode] genericTypes, str name, list[AstNode] typedArguments): {
							if (size(typedArguments)==1){ // check if there is only one typed argument for the method invocation
								switch(typedArguments[0]){
									case methodInvocation(Option[AstNode] optionalExpression, list[AstNode] genericTypes, str name, list[AstNode] typedArguments): {
										// now check if the call chain is equal to determine if the same object is calld
										str thenFinalInvokedMethod = name;
										list[str] thenCallChain = buildMethodCallList(optionalExpression);
										//println("thenFinalMethod: <thenFinalInvokedMethod>, boolFinalMethod: <boolExprFinalInvokedMethod> thenCallChain: <thenCallChain>, boolExprCallChain <boolExprcallChain>");
										if (!(boolExprcallChain==thenCallChain)) return false;// the chain is not equal so its not a match, so rule does not apply
										// now check if the method invoked is the same by replacing the has with a get.
										if (startsWith(thenFinalInvokedMethod,"get")){
											// it is a getter, so check if it the name is the same 
											str tmpThenMethod = replaceFirst(thenFinalInvokedMethod, "get", "");
											str tmpBoolExprMethod = replaceFirst(boolExprFinalInvokedMethod, "has", "");
											//println("thenMethod: <tmpThenMethod> boolExprMethod: <tmpBoolExprMethod>");
											if (tmpThenMethod == tmpBoolExprMethod) return true; // It is the same method, so all the rule apply!!
											else return false;
										}
										else return false;// it does not start with get, so rule does not apply
									}
									default: return false; // the typedArguments was not a methodInvocation.
								}
							}
							else return false; // Only one typed argument allowed but more found, so rule does not apply
						}
					default: return false;// does not contain an methodInvocation, so rule does not apply
				}
			}
			default: return false; // does not contain an expression statement, so rule does not apply
		}
	}
	else return false;// more then one statement so return false
}

private list[str] buildMethodCallList(Option[AstNode] optionalExpression){
	list[str] tmpList = [];
	switch(optionalExpression){
		case none(): {tmpList = tmpList + [];}
		case some(methodStuff): {
			switch (methodStuff){
				case simpleName(str simpleName): {
					tmpList = tmpList + [simpleName];
				}
				case methodInvocation(Option[AstNode] optionalExpression, list[AstNode] genericTypes, str name, list[AstNode] typedArguments): {
					tmpList = tmpList + buildMethodCallList(optionalExpression) + [name];
				}
			}
		}
	}
	return tmpList;
}